use std::{fs, io};
use std::borrow::BorrowMut;
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::ops::Deref;
use std::path::Path;
use std::sync::{Arc, Mutex, RwLock};
use std::sync::atomic::{AtomicUsize, Ordering};
use color_eyre::{eyre::eyre, Result};
use color_eyre::owo_colors::OwoColorize;
use time::{Duration, format_description, OffsetDateTime, Time, UtcOffset};


pub fn minutely(
    offset_time: UtcOffset,
    directory: impl AsRef<Path>,
    file_name_prefix: impl AsRef<Path>,
) -> RollingFileAppender {
    RollingFileAppender::new(offset_time, Rotation::MINUTELY, directory, file_name_prefix)
}

pub fn hourly(
    offset_time: UtcOffset,
    directory: impl AsRef<Path>,
    file_name_prefix: impl AsRef<Path>,
) -> RollingFileAppender {
    RollingFileAppender::new(offset_time, Rotation::HOURLY, directory, file_name_prefix)
}

pub fn daily(
    offset_time: UtcOffset,
    directory: impl AsRef<Path>,
    file_name_prefix: impl AsRef<Path>,
) -> RollingFileAppender {
    RollingFileAppender::new(offset_time, Rotation::DAILY, directory, file_name_prefix)
}

pub fn never(
    offset_time: UtcOffset,
    directory: impl AsRef<Path>,
    file_name: impl AsRef<Path>,
) -> RollingFileAppender {
    RollingFileAppender::new(offset_time, Rotation::NEVER, directory, file_name)
}

#[derive(Clone)]
pub struct RollingFileAppender {
    state: Inner,
    writer: Arc<RwLock<File>>,
    offset_time: UtcOffset,
}

impl RollingFileAppender {
    pub fn new(
        offset_time: UtcOffset,
        rotation: Rotation,
        directory: impl AsRef<Path>,
        file_name_prefix: impl AsRef<Path>,
    ) -> RollingFileAppender {
        let now = OffsetDateTime::now_utc().to_offset(offset_time);
        let (state, writer) = Inner::new(now, rotation, directory, file_name_prefix);
        Self {
            state,
            writer,
            offset_time,
        }
    }

    #[inline]
    fn now(&self) -> OffsetDateTime {
        OffsetDateTime::now_utc().to_offset(self.offset_time)
    }
}

impl io::Write for RollingFileAppender {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        let now = self.now();
        let mut writer = self.writer.write().unwrap();
        if let Some(current_time) = self.state.should_rollover(now) {
            let _did_cas = self.state.advance_date(now, current_time);
            debug_assert!(_did_cas, "if we have &mut access to the appender, no other thread can have advanced the timestamp...");
            self.state.refresh_writer(now, writer.borrow_mut());
        }
        writer.write(buf)
    }

    fn flush(&mut self) -> io::Result<()> {
        self.writer.write().unwrap().flush()
    }
}

#[derive(Clone, Eq, PartialEq, Debug)]
pub struct Rotation(RotationKind);

#[derive(Clone, Eq, PartialEq, Debug)]
enum RotationKind {
    Minutely,
    Hourly,
    Daily,
    Never,
}

impl Rotation {
    /// Provides an minutely rotation
    pub const MINUTELY: Self = Self(RotationKind::Minutely);
    /// Provides an hourly rotation
    pub const HOURLY: Self = Self(RotationKind::Hourly);
    /// Provides a daily rotation
    pub const DAILY: Self = Self(RotationKind::Daily);
    /// Provides a rotation that never rotates.
    pub const NEVER: Self = Self(RotationKind::Never);

    pub(crate) fn next_date(&self, current_date: &OffsetDateTime) -> Option<OffsetDateTime> {
        let unrounded_next_date = match *self {
            Rotation::MINUTELY => *current_date + Duration::minutes(1),
            Rotation::HOURLY => *current_date + Duration::hours(1),
            Rotation::DAILY => *current_date + Duration::days(1),
            Rotation::NEVER => return None,
        };
        Some(self.round_date(&unrounded_next_date))
    }

    // note that this method will panic if passed a `Rotation::NEVER`.
    pub(crate) fn round_date(&self, date: &OffsetDateTime) -> OffsetDateTime {
        match *self {
            Rotation::MINUTELY => {
                let time = Time::from_hms(date.hour(), date.minute(), 0)
                    .expect("Invalid time; this is a bug in tracing-appender");
                date.replace_time(time)
            }
            Rotation::HOURLY => {
                let time = Time::from_hms(date.hour(), 0, 0)
                    .expect("Invalid time; this is a bug in tracing-appender");
                date.replace_time(time)
            }
            Rotation::DAILY => {
                let time = Time::from_hms(0, 0, 0)
                    .expect("Invalid time; this is a bug in tracing-appender");
                date.replace_time(time)
            }
            // Rotation::NEVER is impossible to round.
            Rotation::NEVER => {
                unreachable!("Rotation::NEVER is impossible to round.")
            }
        }
    }

    pub(crate) fn join_date(&self, filename: &str, date: &OffsetDateTime) -> String {
        match *self {
            Rotation::MINUTELY => {
                let format = format_description::parse("[year]-[month]-[day]-[hour]-[minute]")
                    .expect("Unable to create a formatter; this is a bug in tracing-appender");

                let date = date
                    .format(&format)
                    .expect("Unable to format OffsetDateTime; this is a bug in tracing-appender");
                format!("{}.{}", filename, date)
            }
            Rotation::HOURLY => {
                let format = format_description::parse("[year]-[month]-[day]-[hour]")
                    .expect("Unable to create a formatter; this is a bug in tracing-appender");

                let date = date
                    .format(&format)
                    .expect("Unable to format OffsetDateTime; this is a bug in tracing-appender");
                format!("{}.{}", filename, date)
            }
            Rotation::DAILY => {
                let format = format_description::parse("[year]-[month]-[day]")
                    .expect("Unable to create a formatter; this is a bug in tracing-appender");
                let date = date
                    .format(&format)
                    .expect("Unable to format OffsetDateTime; this is a bug in tracing-appender");
                format!("{}.{}", filename, date)
            }
            Rotation::NEVER => filename.to_string(),
        }
    }
}

#[derive(Debug, Clone)]
struct Inner {
    log_directory: String,
    log_filename_prefix: String,
    rotation: Rotation,
    next_date: Arc<AtomicUsize>,
}

impl Inner {
    fn new(
        now: OffsetDateTime,
        rotation: Rotation,
        directory: impl AsRef<Path>,
        file_name_prefix: impl AsRef<Path>,
    ) -> (Self, Arc<RwLock<File>>) {
        let log_directory = directory.as_ref().to_str().unwrap();
        let log_filename_prefix = file_name_prefix.as_ref().to_str().unwrap();

        let filename = rotation.join_date(log_filename_prefix, &now);
        let next_date = rotation.next_date(&now);
        let writer = Arc::new(RwLock::new(
            create_writer(log_directory, &filename).expect("failed to create appender"),
        ));

        let inner = Inner {
            log_directory: log_directory.to_string(),
            log_filename_prefix: log_filename_prefix.to_string(),
            next_date: Arc::new(AtomicUsize::new(
                next_date
                    .map(|date| date.unix_timestamp() as usize)
                    .unwrap_or(0),
            )),
            rotation,
        };
        (inner, writer)
    }

    fn refresh_writer(&self, now: OffsetDateTime, file: &mut File) {
        let filename = self.rotation.join_date(&self.log_filename_prefix, &now);

        match create_writer(&self.log_directory, &filename) {
            Ok(new_file) => {
                if let Err(err) = file.flush() {
                    eprintln!("Couldn't flush previous writer: {}", err);
                }
                *file = new_file;
            }
            Err(err) => eprintln!("Couldn't create writer for logs: {}", err),
        }
    }

    /// Checks whether or not it's time to roll over the log file.
    ///
    /// Rather than returning a `bool`, this returns the current value of
    /// `next_date` so that we can perform a `compare_exchange` operation with
    /// that value when setting the next rollover time.
    ///
    /// If this method returns `Some`, we should roll to a new log file.
    /// Otherwise, if this returns we should not rotate the log file.
    fn should_rollover(&self, date: OffsetDateTime) -> Option<usize> {
        let next_date = self.next_date.load(Ordering::Acquire);
        // if the next date is 0, this appender *never* rotates log files.
        if next_date == 0 {
            return None;
        }

        if date.unix_timestamp() as usize >= next_date {
            return Some(next_date);
        }

        None
    }

    fn advance_date(&self, now: OffsetDateTime, current: usize) -> bool {
        let next_date = self
            .rotation
            .next_date(&now)
            .map(|date| date.unix_timestamp() as usize)
            .unwrap_or(0);
        self.next_date
            .compare_exchange(current, next_date, Ordering::AcqRel, Ordering::Acquire)
            .is_ok()
    }
}

fn create_writer(directory: &str, filename: &str) -> io::Result<File> {
    let path = Path::new(directory).join(filename);
    let mut open_options = OpenOptions::new();
    open_options.append(true).create(true);

    let new_file = open_options.open(path.as_path());
    if new_file.is_err() {
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)?;
            return open_options.open(path);
        }
    }

    new_file
}